module GridHelper
  class Grid

    class GridColumn
      attr_accessor :fld, :options, :lines
      def initialize(grid, fld, options)
        @grid = grid
        @fld = fld
        @options = options
        @options[:separator] ||= "<br/>"
        @lines = []
      end

      def line(*args)
        options = args.last.is_a?(Hash) ? args.pop : {}
        fld = args.shift
        line = GridLine.new(fld, options)
        yield line if block_given?
        @lines << line
      end

      def value(element, first_col = false)
        if @lines.empty?
          text = format_data(multi_send(element, @fld), @options)
          first_col ? link_to_element(element, text) : text
        else
          first_line = true
          @lines.map do |line| 
            line_value = line.value(element)
            text = (first_col && first_line) ? link_to_element(element, line_value) : line_value
            first_line = false
            format_data(text, line.options)
          end.join(@options[:separator])
        end
      end

      def link_to_element(element, text)
        parent = @grid.opciones[:parent]
        if @grid.opciones[:no_autolink]
          rtn = text
        else
          autolink = @grid.opciones[:autolink]
          if autolink.is_a?(Proc) 
            rtn = autolink.call(element)
          end
          rtn ||= @grid.template.link_to(text, @grid.template.my_url_for(@grid.controller, 'show', element, parent))
        end
        rtn
      end

      def format_data(data, options)
        if data.is_a?(Date)
          data.to_time.strftime('%Y-%m-%d')
        elsif data.is_a?(Time)
          data.strftime('%Y-%m-%d %H:%M')
        elsif data.is_a?(TrueClass) || data.is_a?(FalseClass)
          data ? 'Sí' : 'No'
        elsif options[:format] == :currency
          data ? @grid.template.number_to_currency(data, :precision => (options[:precision] || 2)) : ''
        elsif options[:format] == :numeric
          @grid.template.number_with_precision(data || 0, options[:precision] || 2)
        else
          data
        end
      end

    end

    class GridLine
      attr_accessor :fld, :options, :fields
      def initialize(fld, options = {})
        @fld = fld
        @options = options
        @options[:separator] ||= ", "
        @fields = []
      end
      
      def field(*args)
        options = args.last.is_a?(Hash) ? args.pop : {}
        fld = args.shift
        field = GridField.new(fld, options)
        @fields << field
      end

      def value(element)
        v = if @fields.empty?
          multi_send(element, @fld)
        else
          @fields.map {|l| l.value(element)}.join(@options[:separator])
        end
        v = %{<span class="#{@options[:class]}">#{v}</span>} if @options[:class]
        v
      end
    end
    
    class GridField
      attr_accessor :fld, :options
      def initialize(fld, options = {})
        @fld = fld
        @options = options
      end
      
      def value(element)
        cls = @options[:class] ? %{ class="#{@options[:class]}"} : ''
        "<span#{cls}>#{multi_send(element, @fld)}</span>"
      end
    end

    attr_reader :columns, :modelo, :lista, :opciones, :controller, :controller_name, :controller_class, :template
    def initialize(template, modelo, lista, opciones = {})
      @template, @modelo, @lista = template, modelo, lista
      @opciones = opciones.reverse_merge :sortable => true, :edit => true, :new_link => true, :actions => true

      @columns = []
      @controller = opciones[:controller] || @modelo.to_s.pluralize
      @controller_name = (@controller + '_controller').to_sym
      @controller_class = Object::const_get(@controller_name.to_s.classify) rescue nil
    end

    def column(*args)
      options = args.last.is_a?(Hash) ? args.pop : {}
      field = args.shift
      options[:sortable] = @opciones[:sortable] if options[:sortable].nil? && field.is_a?(Symbol)
      options[:sortable] = false if block_given? || options[:value]
      unless options[:label]
        options[:label] = (field.is_a?(Array) ? field.first : field).to_s.humanize
      end
      col = GridColumn.new(self, field, options)
      yield col if block_given?
      @columns << col
    end

    def helper(field, opciones = {})
      column field, opciones.merge(:value => lambda {|e| @template.send(field, e)})
    end
  end 

  def grid_for(*args, &block)
    modelo, lista, opciones = grid_parameters(*args)
    block ||= opciones[:bloque]
    block.call(g = Grid.new(@template, modelo, lista, opciones))
    grid_to_html(g)
  end

  private

  def grid_to_html(g)
    if g.lista
      table_body = table_html(g) + paginator_html(g)
    else
      table_body = content_tag(:div, "No hay registros para mostrar.")
    end
    header_html(g) + table_body
  end

  def header_html(g)
    unless g.opciones[:header] == false
      nombre = g.modelo.to_s
      header = g.opciones[:header] || nombre.pluralize.humanize
      header += model_spinner(g) unless g.opciones[:new_link]
      new_url = g.opciones[:new_link] ? header_new_url(g) : header_new_url_empty(g)
      header = content_tag(:span, content_tag(:h3, header) + new_url,
        :class => 'titulo_centrado')
    else
      header = content_tag(:span, model_spinner(g) + header_new_url_empty(g), :class => 'titulo_centrado')
    end
    header << header_inline_form(g)
  end

  def header_new_url(g)
    link_to_new_url(g) + header_link_to_cancel(g)
  end
  
  def header_new_url_empty(g)
    content_tag(:div, '', :id => new_link_id(g)) + 
    header_link_to_cancel(g)
  end

  def header_link_to_cancel(g)
    buttons = g.controller_class.buttons rescue nil
    txt = button_text(buttons, 'Cancelar', :cancel, nil)
    link_to_cancel(txt, :remote => :true, :model => g.modelo, :hidden => true)
  end

  def link_to_new_url(g)
    url = my_url_for(g.controller, 'new', nil, g.opciones[:parent])
    txt = g.opciones[:new_link_text] || "Agregar #{g.modelo.to_s.humanize}"
    content_tag(:span, link_to_remote(txt, toggle_spinner(model_spinner_id(g), :url => url, 
    :href => url, :method => :get)) +
    model_spinner(g),
    :id => new_link_id(g))
  end

  def model_spinner(g)
    spinner(:id => model_spinner_id(g), :class => 'new_link_spinner')
  end
  
  def new_link_id(g); "#{g.modelo}_new_link"; end
  def model_spinner_id(g); "#{g.modelo}_spinner"; end
  
  def header_inline_form(g)
    if g.opciones[:new_link] || g.opciones[:edit]
      content_tag(:div, '&nbsp;', :id => "#{g.modelo}_inline_form", :style => 'display:none')
    else
      ''
    end
  end
  
  def table_html(g)
    content_tag(:table, cabecera(g) + filas(g), 
    :class => 'wide forums', :border => '0', 
    :cellspacing => '0', :cellpading => '0')
  end
  
  def paginator_html(g)
    paginator = instance_variable_get("@#{g.modelo.to_s.pluralize}_paginator")
    return '' unless paginator && paginator.page_count > 1
    link_prev, link_next = paginator_prev_and_next(paginator)
    pag_links = pagination_links_remote(paginator)
    rtn = content_tag(:span, 
            "Páginas: <strong>#{link_prev}#{pag_links}#{link_next}</strong>",
            :class => 'pages')
    rtn += content_tag(:span,
            "Mostrando #{paginator.current.first_item} - #{paginator.current.last_item} de #{paginator.item_count}",
            :class => 'pages_right')
    rtn += "<br/>" + content_tag(:span, link_to('Ver todos los registros', :no_paginate => 1),
            :class => 'pages')
  end
  
  def paginator_prev_and_next(paginator)
    if paginator.current.previous
      link_prev = link_to_remote(h('< Anterior'), :url => {:action => 'index', :page => paginator.current.previous}, :method => :get) + " | "
    else
      link_prev = ''
    end
    if paginator.current.next
      link_next = ' | ' + link_to_remote(h('Siguiente >'), :url => {:action => 'index', :page => paginator.current.next}, :method => :get)
    else
      link_next = ''
    end
    [link_prev, link_next]
  end
  
  def pagination_links_remote(paginator)
    pagination_links_each(paginator, :window_size => 4) do |n|
      options = {:url => {:action => 'index', :page => n}, :method => :get}
      link_to_remote n.to_s, options
    end
  end
  
  def cabecera(g)
    content_tag('tr', titulos(g))
  end
  
  def titulos(g)
    rtn = []
    g.columns.each do |col|
      rtn << content_tag('th', titulo_columna(g, col)) 
    end
    if actions_column?(g)
      rtn << content_tag('th', 'Acciones') 
    end
    rtn.join("\n")
  end
  
  def titulo_columna(g, col)
    texto = if col.options[:sortable]
      order_key = "order_#{g.modelo}".to_sym
      orden = (actual(g, col.fld) && session[order_key] != " DESC") ? "down" : "up"
      link_to_remote(col.options[:label], toggle_spinner("#{g.modelo}_spinner", 
        :url => sort_url(g, col, orden),
        :method => :get))
    else
      orden = nil
      col.options[:label]
    end
    content_tag 'span', texto, :class => class_titulo_columna(g, col.fld, orden)
  end
  
  def sort_url(g, col, orden)
    controller.my_url_for_hash(g.controller, 'index', nil, g.opciones[:parent], 
      :sort_by => col.fld, :order => orden, 
      :filter => session["#{g.modelo}_filter".to_sym])
  end
  
  def actual(g, field)
    sort_key = "sort_by_#{g.modelo}".to_sym
    session[sort_key] == field.to_s
  end
  
  def class_titulo_columna(g, field, orden)
    if orden.nil?
      "not_sort"
    else
      "sort_#{actual(g, field) ? orden : 'none'}"
    end
  end
  
  def filas(g)
    g.lista.map{|e| fila(g, e)}.join
  end
  
  def fila(g, elemento)  
    fields = []
    first_col = true
    actual = (params[:id] == elemento.id)
    
    for col in g.columns  
      clase = field_class(g, col)
      if valor = col.options[:value]
        valor = valor.call(elemento) if valor.is_a?(Proc)
      else
        valor = col.value(elemento, first_col)
      end
      first_col = false
      texto = valor
      if params[:search] && g.controller_class.search_fields.include?(col.fld)
        texto = highlight(texto, params[:search]) 
      end

      fields << content_tag(:td, texto, :class => clase)
    end
    if actions_column?(g)
      fields << content_tag(:td, action_links(g, elemento)) 
    end
    content_tag(:tr, fields.join("\n"), :class => "hentry #{' marcar' if actual}", :id => elemento.id_) 
  end
  
  def field_class(g, col)
    cls = actual(g, col.fld) ? "inv" : nil
    is_numeric = [:currency, :numeric].include?(col.options[:format])
    cls = cls.to_s + (is_numeric ? " ra" : "")
    cls.empty? ? nil : cls
  end
  
  def action_links(g, elemento)
    actions = []
    parent = g.opciones[:parent]
    editable = if g.opciones[:edit].is_a?(Proc)
      g.opciones[:edit].call(elemento)
    else
      g.opciones[:edit]
    end
    if editable
      url = my_url_for_hash(g.controller, 'edit', elemento, parent).merge :inline => true
      link = link_to_remote('Editar', toggle_spinner("#{g.modelo}_spinner", :url => url, :method => :get))
      actions << content_tag(:span, link, :class => 'edit_link')
    end
    unless g.opciones[:actions] == false
      actions << action_links_array(elemento, :model => g.modelo, :controller_name => g.controller_name).map {|link| content_tag(:span, link, :class => 'action_link')}
    end
    actions.join(' ')    
  end
  
  def actions_column?(g)
    g.controller_class && g.opciones[:actions] && (!g.controller_class.actions.empty? || g.opciones[:edit])
  end
  
end

ActionView::Base.send :include, GridHelper